﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace DecisionTree
{
    [Serializable]
    public class DecisionTree
    {
        public int TrainError { get; set; }
        public int TestError { get; set; }

        public override string ToString()
        {
            return "Tree " + TreeIndex.ToString();
        }

        public int TreeIndex { get; set; }

        /// <summary>
        ///   Gets or sets the root node for this tree.
        /// </summary>
        /// 
        public DecisionNode Root { get; set; }

        /// <summary>
        ///   Gets the collection of attributes processed by this tree.
        /// </summary>
        /// 
        public DecisionAttributeCollection Attributes { get; private set; }

        /// <summary>
        ///   Gets the number of distinct output
        ///   classes classified by this tree.
        /// </summary>
        /// 
        public byte OutputClasses { get; private set; }

        /// <summary>
        ///   Gets the number of input attributes
        ///   expected by this tree.
        /// </summary>
        public int InputCount { get; private set; }

        /// <summary>
        ///   Creates a new <see cref="DecisionTree"/> to process
        ///   the given <paramref name="attributes"/> and the given
        ///   number of possible <paramref name="outputClasses"/>.
        /// </summary>
        /// 
        /// <param name="attributes">An array specifying the attributes to be processed by this tree.</param>
        /// <param name="outputClasses">The number of possible output classes for the given atributes.</param>
        /// 
        public DecisionTree(DecisionVariable[] attributes, byte outputClasses)
        {
            if (outputClasses <= 0)
                throw new ArgumentOutOfRangeException("outputClasses");
            if (attributes == null)
                throw new ArgumentNullException("attributes");

            this.Attributes = new DecisionAttributeCollection(attributes);
            this.InputCount = attributes.Length;
            this.OutputClasses = outputClasses;
        }

        public DecisionTree(int[][] inputRanges, byte outputClasses, int treeIndex = -1)
        {
            int numAttributes = 0;
            for (int i = 0; i < inputRanges.Length; i++)
                numAttributes += inputRanges[i][0];
            
            this.TreeIndex = treeIndex;

            DecisionVariable[] attributes = new DecisionVariable[numAttributes];

            int lastIndex = 0;
            for (int i = 0; i < inputRanges.Length; i++)
            {
                for (int j = 0; j < inputRanges[i][0]; j++)
                {
                    attributes[lastIndex] = new DecisionVariable(lastIndex, new Range(0, inputRanges[i][1] - 1));
                    lastIndex++;
                }
            }

            this.Attributes = new DecisionAttributeCollection(attributes);
            this.InputCount = attributes.Length;
            this.OutputClasses = outputClasses;
        }

        /// <summary>
        ///   Computes the decision for a given input.
        /// </summary>
        /// 
        /// <param name="input">The input data.</param>
        /// 
        /// <returns>A predicted class for the given input.</returns>
        /// 
        public int Compute(byte[] input)
        {
            if (Root == null)
                throw new InvalidOperationException();

            DecisionNode current = Root;

            // Start reasoning
            while (current != null)
            {
                // Check if this is a leaf
                if (current.IsLeaf)
                {
                    return current.Output.Value;
                }

                // This node is not a leaf. Continue the
                // decisioning proccess following the childs

                // Check which child is responsible for dealing
                // which the particular value of the attribute
                DecisionNode nextNode = null;

                bool result = current.Compute(input);

                if (result)
                    nextNode = current.Branches[1];
                else
                    nextNode = current.Branches[0];

                current = nextNode;
            }

            // Normal execution should not reach here.
            throw new InvalidOperationException("The tree is degenerated.");
        }
        
        public void Log(StreamWriter writer)
        {
            List<int> leafDistances = TreeStatistics.GetLeafDistances(this);

            double average = leafDistances.Average();
            double min = leafDistances.Min();
            double max = leafDistances.Max();
            double stdVar = leafDistances.StandardDeviation();

            writer.WriteLine(this + " statistics:");
            writer.WriteLine("----------------");
            writer.WriteLine("Train error: " + this.TrainError);
            writer.WriteLine("Test error: " + this.TestError);
            writer.WriteLine("Average path: " + average);
            writer.WriteLine("Min path: " + min);
            writer.WriteLine("Max path: " + max);
            writer.WriteLine("Standard deviation path: " + stdVar);
        }
    }
}
